package com.xiaolin.ssosystem.service.impl;

import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.xiaolin.ssosystem.constants.RedisKey;
import com.xiaolin.ssosystem.enums.LockStatus;
import com.xiaolin.ssosystem.mapper.UserLockRecordMapper;
import com.xiaolin.ssosystem.model.dto.LogoutDto;
import com.xiaolin.ssosystem.model.dto.UserInfoFetch;
import com.xiaolin.ssosystem.model.entity.User;
import com.xiaolin.ssosystem.model.dto.UserDto;
import com.xiaolin.ssosystem.model.entity.UserLockRecord;
import com.xiaolin.ssosystem.service.SSOService;
import com.xiaolin.ssosystem.service.UserService;
import com.xiaolin.ssosystem.utils.ContextUtil;
import com.xiaolin.ssosystem.utils.OptResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author xiaolin
 * @description 描述
 * @since 2024/10/9
 */
@Slf4j
@Service
public class SSOServiceImpl implements SSOService {
    @Autowired
    private UserService userService;
    @Autowired
    private UserLockRecordMapper userLockRecordMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final String ssoToken = "sso-token";

    private Map<String, String> appKeyMap = Collections.singletonMap("915602c45db74b7ca6c3bb5fcd6dfdfe", "b7fc288a5f234315b18a293fad0aeea8");

    private Set<String> nonceSet = new HashSet<>();

    @Override
    public OptResult login(UserDto userDto, HttpServletResponse response) {
        // 校验用户名密码是否正确
        String userAccount = userDto.getUserAccount();
        // 校验用户是否被锁定
        UserLockRecord userLockRecord = userLockRecordMapper.selectOne(new LambdaQueryWrapper<UserLockRecord>()
                .eq(UserLockRecord::getUserAccount, userAccount));
        if (Objects.nonNull(userLockRecord) && LockStatus.LOCKED.getCode().equals(userLockRecord.getLockStatus())) {
            return new OptResult(400, null);
        }
        User user = userService.getOne(new LambdaQueryWrapper<User>()
                .eq(User::getUserAccount, userAccount)
                .eq(User::getPwd, userDto.getPwd()));
        if (Objects.isNull(user)) {
            // 记录用户账号密码错误次数
            if (Objects.isNull(userLockRecord)) {
                userLockRecord = new UserLockRecord();
                userLockRecord.setUserAccount(userAccount);
                userLockRecord.setErrorCount(1);
                userLockRecordMapper.insert(userLockRecord);
            } else {
                userLockRecordMapper.update(new UserLockRecord(), new UpdateWrapper<UserLockRecord>().lambda()
                        .set(UserLockRecord::getErrorCount, userLockRecord.getErrorCount() + 1)
                        .set(userLockRecord.getErrorCount() + 1 == 10, UserLockRecord::getLockStatus, LockStatus.LOCKED.getCode())
                        .eq(UserLockRecord::getUserAccount, userAccount));
            }
            return new OptResult(400, null);
        }
        // 更新用户输入密码错误次数
        userLockRecordMapper.update(new UserLockRecord(), new UpdateWrapper<UserLockRecord>().lambda()
                .set(UserLockRecord::getErrorCount, 0)
                .eq(UserLockRecord::getUserAccount, userAccount));
        String token = UUID.randomUUID().toString();
        // 保存用户信息到Redis
        redisTemplate.opsForValue().set(token, user, 30, TimeUnit.MINUTES);
        // 用户账号关联token
        redisTemplate.opsForHash().put(RedisKey.LOGIN_FLAG + user.getUserAccount(),
                ssoToken, token);
        // 给前端派发一个cookie
        Cookie cookie = new Cookie(ssoToken, token);
        // 设置域名
        cookie.setDomain("sso.system.com");
        // 设置路径
        cookie.setPath("/");
        // 设置超时时间
        long seconds = TimeUnit.MINUTES.toSeconds(60 * 24);
        cookie.setMaxAge((int) seconds);
        response.addCookie(cookie);
        return new OptResult(200, null);
    }

    @Override
    public OptResult getCode(HttpServletRequest request) {
        Map<String, Object> data = new HashMap<>();
        // 1.校验cookie是否存在
        String token = ContextUtil.getToken(request, ssoToken);
        if (StringUtils.isEmpty(token)) {
            return new OptResult(400, data);
        }
        // 2.校验用户是否已登录
        User user = (User) redisTemplate.opsForValue().get(token);
        if (Objects.isNull(user)) {
            return new OptResult(400, null);
        }
        // 3.生成一次性令牌
        String code = UUID.randomUUID().toString();
        // 4.关联令牌与用户信息，令牌5分钟有效
        redisTemplate.opsForValue().set(code, user, 5, TimeUnit.MINUTES);
        // 5.返回令牌
        data.put("data", code);
        return new OptResult(200, data);
    }

    @Override
    public OptResult info(UserInfoFetch userInfoFetch, HttpServletRequest request) {
        // 0.校验签名
        // 0.1 校验appKey是否存在（假设已经从数据库查出数据）
        String appKey = request.getHeader("appKey");
        String appSecret = appKeyMap.get(appKey);
        if (StringUtils.isEmpty(appSecret)) {
            log.error("秘钥不存在。。。");
            return new OptResult(400, null);
        }
        // 0.2 校验时间戳是否在五分钟内
        String timestamp = request.getHeader("timestamp");
        long curTime = new Date().getTime();
        if (StringUtils.isEmpty(timestamp) || (Long.parseLong(timestamp) + 1000 * 60 * 5 < curTime)) {
            log.error("请求已过期。。。");
            return new OptResult(400, null);
        }
        // 0.3 校验随机数是否已使用（假设已经将使用过的随机数保存到数据库中）
        String nonce = request.getHeader("nonce");
        if (nonceSet.contains(nonce)) {
            log.error("请求重复。。。");
            return new OptResult(400, null);
        }
        nonceSet.add(nonce);
        // 0.4 将请求参数与appSecret使用签名算法加密处理
        String sign = DigestUtil.sha256Hex(JSONUtil.toJsonStr(userInfoFetch) + appSecret);
        // 0.5 比对签名是否一致
        String headerSign = request.getHeader("sign");
        if (!sign.equals(headerSign)) {
            log.error("签名错误。。。");
            return new OptResult(400, null);
        }
        // 0.6 处理逻辑
        String code = userInfoFetch.getCode();
        // 1.校验令牌是否合法
        User user = (User) redisTemplate.opsForValue().get(code);
        if (Objects.isNull(user)) {
            return new OptResult(400, null);
        }
        // 2.删除令牌
        redisTemplate.delete(code);
        // 保存用户登录的系统
        redisTemplate.opsForHash().put(RedisKey.LOGIN_FLAG + user.getUserAccount(),
                userInfoFetch.getSystemFlag(), userInfoFetch.getToken());
        // 3.返回用户信息
        Map<String, Object> data = new HashMap<>();
        data.put("userInfo", user);
        return new OptResult(200, data);
    }

    @Override
    public OptResult logout(LogoutDto logoutDto) {
        // 1.校验用户是否登录过期，如果未登录过期则删除会话信息
        String userAccount = logoutDto.getUserAccount();
        Map<Object, Object> loginFlagMap = redisTemplate.opsForHash().entries(RedisKey.LOGIN_FLAG + userAccount);
        if (!CollectionUtils.isEmpty(loginFlagMap)) {
            String token = (String) loginFlagMap.get(ssoToken);
            redisTemplate.delete(token);
        }
        // 2.遍历需要注销的系统，http调用系统的注销回调接口
        for (Map.Entry<Object, Object> entity : loginFlagMap.entrySet()) {
            if (!ssoToken.equals(entity.getKey())) {
                try {
                    String systemFlag = (String) entity.getKey();
                    String clientToken = (String) entity.getValue();
                    HttpRequest post = HttpUtil.createPost(systemFlag + clientToken);
                    HttpResponse execute = post.execute();
                    System.out.println(execute);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return new OptResult(200, null);
    }
}
